/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package main;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import weka.core.Instances;
import weka.core.OptionHandler;
import weka.core.TechnicalInformation;
import weka.core.TechnicalInformationHandler;
import weka.core.WeightedInstancesHandler;
import weka.classifiers.Classifier;
import weka.core.Attribute;
import weka.core.Instance;
import weka.core.converters.ArffLoader;

/**
 *
 * @author Reinhard
 */
public class MyNaiveBayes implements MyClassifier {
  public Instances inst;
  public ArrayList<Instances> train, test;
  public ArrayList<Boolean> isDiscretized;
  public ArrayList<String> className;
  public ArrayList<Instances> segment;
  public ArrayList<ArrayList<Double>> probTable, meanTable, varTable;
  public HashMap<String, Double> probClass;
  private double accuracy;
  private int correct = 0;

  public MyNaiveBayes() {
    
  }
  
  /**
   * Main constructor
   * @param inst 
   */
  public MyNaiveBayes(Instances inst) {
    this.inst = inst;
    probTable = new ArrayList<ArrayList<Double>>();
    meanTable = new ArrayList<ArrayList<Double>>();
    varTable = new ArrayList<ArrayList<Double>>();

    // divide Instances to segment
    Instances[] seg = splitData(inst, inst.classAttribute());
    segment = new ArrayList<Instances>(Arrays.asList(seg));
  }

  /**
   * Set the training set
   * @param inst Instances to be used to train the classifier.
   */
  public void setInstances(Instances inst) {
    this.inst = inst;
  }

  /**
   * Starts creating the Naive Bayes model
   */
  public void doLearn() {
    // build probability table
    for (int i = 0; i < inst.numAttributes() - 1; ++i) {
      // probability satu atribut ke-i jika diketahui kelas c
      ArrayList<Double> probEach = new ArrayList<Double>();
      ArrayList<Double> meanEach = new ArrayList<Double>();
      ArrayList<Double> varEach = new ArrayList<Double>();
      for (int j = 0; j < segment.size(); ++j) {
//        System.out.println(j);
        double x = segment.get(j).instance(i).value(i);
        double avg = getMean(i, j);
        double var = getVariance(i, j);
        probEach.add(prob(x, avg, var));
        meanEach.add(avg);
        varEach.add(var);
      }
      probTable.add(probEach);
      meanTable.add(meanEach);
      varTable.add(varEach);
    }
//    System.out.println();
//    System.out.println(probTable);
  }

  /**
   * Return the probability for attribute x, when class c is known
   * 
   * @param x attribute value
   * @param avg mean of attribute x
   * @param var variance of attribute x
   * @return a double denoting the probability
   */
  public double prob(double x, double avg, double var) {
    return (1.0 / Math.sqrt(2 * Math.PI * var * var)) * Math.exp(-(x - avg) * (x - avg) / (2 * var * var));
  }

  /**
   * Splits a dataset according to the values of a nominal attribute.
   *
   * @param data the data which is to be split
   * @param att the attribute to be used for splitting
   * @return the sets of instances produced by the split
   */
  private Instances[] splitData(Instances data, Attribute att) {
    Instances[] splitData = new Instances[att.numValues()];
    for (int j = 0; j < att.numValues(); j++) {
      splitData[j] = new Instances(data, data.numInstances());
    }
    Enumeration instEnum = data.enumerateInstances();
    while (instEnum.hasMoreElements()) {
      Instance inst = (Instance) instEnum.nextElement();
      splitData[(int) inst.value(att)].add(inst);
    }
    for (int i = 0; i < splitData.length; i++) {
      splitData[i].compactify();
    }
    return splitData;
  }

  /**
   * Get the mean of attribute att of segment
   * @param att index of attribute
   * @param segment index of segment
   * @return mean value of the attribute
   */
  public double getMean(int att, int sid) {
    double ret = 0.0;
    for (int i = 0; i < segment.get(sid).numInstances(); ++i) {
      ret += segment.get(sid).instance(i).value(att);
    }
    ret /= segment.get(sid).numInstances();
    return ret;
  }

  /**
   * Get the variance of attribute att of segment
   * @param att index of attribute
   * @param segment index of segment
   * @return variance value of the attribute
   */
  public double getVariance(int att, int sid) {
    double ret = 0.0;
    double mean = getMean(att, sid);
    for (int i = 0; i < segment.get(sid).numInstances(); ++i) {
      double val = segment.get(sid).instance(i).value(att);
      ret += (val - mean) * (val - mean);
    }
    return Math.sqrt(ret/ segment.get(sid).numInstances());
  }

  @Override
  /**
   * Return the model created by NiaveBayes classifier
   */
  public String getModel() {
    StringBuilder sb = new StringBuilder();
    sb.append("NaiveBayes Classifier Model\n\n");
    sb.append("\t\t\tClass\n");
    sb.append("Attribute\t\t");
    // tulis nama segment class
    for(int i = 0; i < segment.size(); ++i) {
      sb.append(segment.get(i).instance(0).stringValue(segment.get(i).classIndex()));
      sb.append("\t");
    }
    sb.append("\n");
    sb.append("=====================================================================================");
    sb.append("\n");
    for(int i = 0; i < inst.numAttributes() - 1; ++i) {
      String attribute_blom_parsed = inst.instance(0).attribute(i).toString();
      String[] array_helper = attribute_blom_parsed.split("\\s+");
      String attribute_parsed = array_helper[1];
      sb.append(attribute_parsed);
      sb.append("\n");
      sb.append(" mean\t\t\t");
      
      for(int j = 0; j < meanTable.get(i).size(); ++j) {
        sb.append(meanTable.get(i).get(j));
        sb.append("\t");
      }
      
      sb.append("\n");
      sb.append(" std. dev.\t\t");
      
      for(int j = 0; j < varTable.get(i).size(); ++j) {
        sb.append(varTable.get(i).get(j));
        sb.append("\t");
      }
      sb.append("\n");
    }
    return sb.toString();
  }

  @Override
  /**
   * Return the accuracy of NaiveBayes classifier
   */
  public double getAccuracy() {
    return accuracy*100;
  }

  @Override
  public void doClassification(Instances test) {
    correct = 0;
    for (int i = 0; i < test.numInstances(); ++i) {
      double maks = Double.MIN_VALUE;
      String res = "";
      for (int j = 0; j < segment.size(); ++j) {
        double pro = 1.0;
        for (int k = 0; k < segment.get(j).numAttributes() - 1; ++k) {
//          System.out.println(k);
          double x = test.instance(i).value(k);
//          System.out.println("x = " + x);
          double avg = getMean(k, j);
//          System.out.println("avg = " + avg);
          double var = getVariance(k, j);
//          System.out.println("var = " + var);
          if (var < 1e-9) {  // hacks : avoid divide by 0!!!
            continue;
          }
          pro *= prob(x, avg, var);
//          System.out.println("prob = " + prob(x,avg,var));
        }
//        System.out.println("pro = " + pro);
//        System.out.println("maks = " + maks);
        if (pro > maks) {
          res = segment.get(j).instance(0).stringValue(segment.get(j).classIndex());
          maks = pro;
        }
      }
//      System.out.println(res + " " + test.instance(i).stringValue(test.classIndex()));
      if (res.equals(test.instance(i).stringValue(test.classIndex()))) {
        ++correct;
      }
    }
//    System.out.println(correct + " " + test.numInstances());
    accuracy = (double) correct / test.numInstances();
  }

  @Override
  public void doCrossValidation(int folds) {
    Instances awal = new Instances(inst);
    ArrayList<Instances> ai = new ArrayList<Instances>();
    int bagi = awal.numInstances() / folds;
    int sisa = awal.numInstances() % folds;
    for (int i = 0; i < folds; ++i) {
      Instances add;
      if (i == folds - 1 && sisa != 0) {
        add = new Instances(awal, i * bagi, sisa);
      } else {
        add = new Instances(awal, i * bagi, bagi);
      }
      ai.add(add);
    }

    double final_accuracy = 0.0;
    for (int i = 0; i < ai.size(); ++i) {
      Instances newtest = ai.get(i);
      Instances newtrain = ai.get(i); // dummy
      boolean pertamax = true;
      for (int j = 0; j < ai.size(); ++j) {
        if (i != j) {
          if (pertamax) {
            newtrain = new Instances(ai.get(j));
            pertamax = false;
            continue;
          }
          for (int k = 0; k < ai.get(j).numInstances(); ++k) {
            newtrain.add(ai.get(j).instance(k));
          }
        }
      }
      setInstances(newtrain);
      doClassification(newtest);
//      System.out.println(accuracy + " ");
      final_accuracy += accuracy;
    }

    final_accuracy /= folds;
    accuracy = final_accuracy;
  }

  @Override
  /**
   * Do perecentage split
   * @param percent percentage of data training to be used for learning. 100-percent % will be used for test.
   */
  public void doPercentageSplit(double percent) {
    inst.randomize(new Random(1));
    int trainSize = (int) Math.round(inst.numInstances() * percent / 100);
    int testSize = inst.numInstances() - trainSize;
    Instances newtrain = new Instances(inst, 0, trainSize);
    Instances newtest = new Instances(inst, trainSize, testSize);
    setInstances(newtrain);
    doClassification(newtest);
  }

  public static void main(String[] args) throws IOException {
    String filename = "D:\\Kuliah\\Semester 6\\IF3054 - Intelegensia Buatan\\WEKA\\segmentation_data.arff";
    ArffLoader loader = new ArffLoader();
    Instances inst;
    loader.setSource(new File(filename));
    inst = loader.getDataSet();
    inst.setClassIndex(inst.numAttributes() - 1);

    System.out.println("Use Training Set");
    MyNaiveBayes nb = new MyNaiveBayes(inst);
    nb.doLearn();
    nb.doClassification(inst);
    System.out.println(nb.getAccuracy());

    System.out.println("Cross Validation");
    nb.doLearn();
    nb.setInstances(inst);
    nb.doCrossValidation(10);
    System.out.println(nb.getAccuracy());

    System.out.println("Percentage Split");
    nb.setInstances(inst);
    nb.doPercentageSplit(66);
    System.out.println(nb.getAccuracy());

    System.out.append(nb.getModel());
  }
}
